namespace Stee.CAP8.DBAccess
{
    using LinqKit;
    using Microsoft.EntityFrameworkCore;
    using Stee.CAP8.Entity;
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Threading;
    using System.Threading.Tasks;

    public class Repository<T> : BaseRepository, IRepository<T>  where T : BaseEntity
    {
        protected DbContext DbContext { get; set; }
        protected DbSet<T> DbSet { get; set; }
        private readonly string entityType = typeof(T).Name.ToString(CultureInfo.InvariantCulture);
        /// <summary>
        /// The constructor uses the unitofwork's context to create DbSet in the Repository
        /// </summary>
        public Repository(DbContext dbContext)
        {
            DbContext = dbContext ?? throw new NoDbContextException("No DbContext is found in Repository");
            DbSet = DbContext.Set<T>();
        }
        /// <summary>
        /// Adds an entity into the database.
        /// <para>This method can be overridden by any class that inherits it.</para>
        /// </summary>
        virtual public void Add(T entity)
        {
            Logger.Trace($"Add Parameter: entity ({entity})");
            DbSet.Add(entity);
            if (entity != null)
            {
                Logger.Trace(entityType + " entity (" + entity.ID + ") is added using Add(T).");
            }
        }
        /// <summary>
        /// Adds an entity into the database.
        /// <para>Use SaveChangesAsync() to add an entity asynchronously</para>
        /// <para>This method is async only to allow special value generators, such as the one used by 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', to access the database asynchronously. For all other cases the non async method should be used.</para>
        /// <para>This method can be overridden by any class that inherits it.</para>
        /// </summary>
        virtual public async Task AddAsync(T entity, CancellationToken cancellationToken = default)
        {
            Logger.Trace($"AddAsync Parameter: entity ({entity})");
            await DbSet.AddAsync(entity, cancellationToken).ConfigureAwait(false);
            if (entity != null)
            {
                Logger.Trace(entityType + " entity (" + entity.ID + ") is added using AddAsync(T).");
            }
        }
        /// <summary>
        /// Adds a variable number of entities to the database without the need to create a collection for them
        /// <para>This method can be overridden by any class that inherits it.</para>
        /// </summary>
        virtual public void AddRange(params T[] entities)
        {
            Logger.Trace($"AddRange(T[]) Parameter: entities ({entities})");
            DbSet.AddRange(entities);
            if (entities != null)
            {
                Logger.Trace("Number of parameter " + entityType + " entities are added using AddRange(T[]).");
            }
        }
        /// <summary>
        /// Adds the elements of the specified collection to the end of the List<T>.
        /// <para>This method can be overridden by any class that inherits it.</para>
        /// </summary>
        virtual public void AddRange(IEnumerable<T> entities)
        {
            Logger.Trace($"AddRange(IEnumerable<T>) Parameter: entities ({entities})");
            DbSet.AddRange(entities);
            if (entities != null)
            {
                Logger.Trace("List of " + entityType + " entities are added using AddRange(IEnumerable<T>).");
            }
        }

        /// <summary>
        /// Adds a variable number of entities to the database without the need to create a collection for them.
        /// <para>Use SaveChangesAsync() to add the variable number of entities asynchronously</para>
        /// <para>This method is async only to allow special value generators, such as the one used by 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', to access the database asynchronously. For all other cases the non async method should be used.</para>
        /// <para>This method can be overridden by any class that inherits it.</para>
        /// </summary>
        virtual public Task AddRangeAsync(CancellationToken cancellationToken = default, params T[] entities)
        {
            Logger.Trace($"AddRangeAsync(T[]) Parameter: entities ({entities})");
            var addRangeTask = DbSet.AddRangeAsync(entities, cancellationToken);
            if (entities != null)
            {
                Logger.Trace("Number of parameter " + entityType + " entities are added using AddRangeAsync(T[]).");
            }
            return addRangeTask;
        }
        /// <summary>
        /// Adds the elements of the specified collection to the end of the List<T>.
        /// <para>Use SaveChangesAsync() to add the collection of entities asynchronously</para>
        /// <para>This method is async only to allow special value generators, such as the one used by 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', to access the database asynchronously. For all other cases the non async method should be used.</para>
        /// <para>This method can be overridden by any class that inherits it.</para>
        /// </summary>
        virtual public Task AddRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default)
        {
            Logger.Trace($"AddRangeAsync(IEnumerable<T>) Parameter: entities ({entities})");
            var addRangeTask = DbSet.AddRangeAsync(entities, cancellationToken);
            if (entities != null)
            {
                Logger.Trace("List of " + entityType + " entities are added using AddRangeAsync(IEnumerable<T>).");
            }
            return addRangeTask;
        }

        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// update an entity in the database
        /// Use SaveChangesAsync to update asynchronously
        /// </summary>
        virtual public void Update(T entity)
        {
            Logger.Trace($"Update Parameter: entity ({entity})");
            DbSet.Update(entity);
            if (entity != null && entity.DataState != DataState.Deleted)
            {
                Logger.Trace(entityType + " entity (" + entity.ID + ") is updated using Update(T).");
            }
        }
        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// update a variable number of entities to the database without the need to create a collection for them
        /// Use SaveChangesAsync to update asynchronously
        /// </summary>
        virtual public void UpdateRange(params T[] entities)
        {
            Logger.Trace($"UpdateRange(T[]) Parameter: entities ({entities})");
            DbSet.UpdateRange(entities);
            if (entities != null)
            {
                Logger.Trace("Number of parameter " + entityType + " entities are updated using UpdateRange(T[]).");
            }
        }
        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// update the elements of the specified collection to the end of the List<T>.
        /// Use SaveChangesAsync to update asynchronously
        /// </summary>
        virtual public void UpdateRange(IEnumerable<T> entities)
        {
            Logger.Trace($"UpdateRange(IEnumerable<T>) Parameter: entities ({entities})");
            DbSet.UpdateRange(entities);
            if (entities != null)
            {
                Logger.Trace("List of " + entityType + " entities are updated using UpdateRange(IEnumerable<T>).");
            }
        }
        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// Synchronously, retrieve a record by its with parameter id
        /// Set Property name with string propertyName
        /// Set Property value with object value
        /// </summary>
        virtual public void UpdateProperty(long id, string propertyName, object value)
        {
            Logger.Trace($"UpdateProperty Parameter: id ({id}, propertyName ({propertyName}, value({value})");
            var item = GetSingle(id);
            // item will never be null, because EF Core will throw exception
            item.GetType().GetProperty(propertyName).SetValue(item, value);
            Logger.Trace(entityType + " entity (" + item.ID + ") is updated by property name using UpdateProperty.");
        }
        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// <para>Synchronously, delete an entity from the database</para>
        /// <para><paramref name="softDelete"/> = TRUE (default), sets record's Datastate to deleted but remain in database</para>
        /// <para><paramref name="softDelete"/> = FALSE, deletes the record from the database</para>
        /// </summary>
        virtual public void Delete(T entity, bool softDelete = true)
        {
            Logger.Trace($"Delete Parameter: entity ({entity}, softDelete ({softDelete})");
            if (softDelete)
            {
                if (entity != null)
                {
                    entity.DataState = DataState.Deleted;
                }
                Update(entity);
                if (entity != null)
                {
                    Logger.Trace(entityType + " entity (" + entity.ID + ")'s datastate has been set to deleted by using Delete(T).");
                }
            }
            else
            {
                DbSet.Remove(entity);
                if (entity != null)
                {
                    Logger.Trace(entityType + " entity (" + entity.ID + ")'s is deleted from the database by using Delete(T).");
                }
            }

        }
        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// <para>Synchronously, delete an entity from the database by finding its ID</para>
        /// <para><paramref name="softDelete"/> = TRUE (default), sets record's Datastate to deleted but remain in database</para>
        /// <para><paramref name="softDelete"/> = FALSE, deletes the record from the database</para>
        /// </summary>
        virtual public void DeleteByID(long id, bool softDelete = true)
        {
            Logger.Trace($"DeleteByID Parameter: id ({id}, softDelete ({softDelete})");
            // item will never be null, because EF Core will throw exception
            var item = GetSingle(id);
            if (softDelete)
            {
                item.DataState = DataState.Deleted;
                Update(item);
                Logger.Trace(entityType + " entity (" + item.ID + ")'s datastate has been set to deleted by using DeleteByID.");
            }
            else
            {
                DbSet.Remove(item);
                Logger.Trace(entityType + " entity (" + item.ID + ")'s is deleted from the database by using DeleteByID.");
            }
        }
        //TODO delete range
/*
        /// <summary>
        ///
        /// This method can be overridden by any class that inherits it
        /// Synchronously, delete a variable number of entities from the database without the need to create a collection for them
        /// </summary>
        virtual public void DeleteRange(params T[] entities)
        {
            throw new NotImplementedException();
        }
        /// <summary>
        /// This method can be overridden by any class that inherits it
        /// Synchronously, delete the elements of the specified collection to the end of the List<T> from the database
        /// </summary>
        virtual public void DeleteRange(IEnumerable<T> entities)
        {
            throw new NotImplementedException();
        }
*/
        /// <summary>
        /// Synchronously, query a record from the database with a condition LinQ expression.
        /// <para>If there are more than one record, EF core will throw exception. </para>
        /// <para>Example: GetSingle(x => x.ID.ToString(CultureInfo.InvariantCulture) == "ABC");</para>
        /// </summary>
        public T GetSingle(Expression<Func<T, bool>> condition = null)
        {
            Logger.Trace($"GetSingle Parameter: condition ({condition})");
            IQueryable<T> query = DbSet;

            if (condition != null)
            {
                query = query.Where(condition);
            }
            var result = query.Single();
            // result will never be null, because EF Core will throw exception
            Logger.Trace(entityType + " entity (" + result.ID + $") is read by GetSingle with condition ({condition}).");
            return result;
        }
        /// <summary>
        /// Synchronously, query a record from the database by using the long.
        /// <para>If there are more than one record, EF core will throw exception. </para>
        /// </summary>
        public T GetSingle(long id)
        {
            Logger.Trace($"GetSingle Parameter: id ({id})");
            var result = DbSet.Where(x => x.ID == id).Single();
            // result will never be null, because EF Core will throw exception
            Logger.Trace(entityType + " entity (" + result.ID.ToString(CultureInfo.InvariantCulture) + ") is read by GetSingle(id).");
            return result;
        }
        /// <summary>
        /// Asynchronously, query a record from the database with a condition LinQ expression
        /// <para>If there are more than one record, EF core will throw exception. </para>
        /// <para>Example: GetSingleAsync(x => x.ID.ToString(CultureInfo.InvariantCulture) == "ABC");</para>
        /// </summary>
        public Task<T> GetSingleAsync(
            Expression<Func<T, bool>> condition = null,
            CancellationToken cancellationToken = default)
        {
            Logger.Trace($"GetSingleAsync Parameter: condition ({condition})");
            IQueryable<T> query = DbSet;

            if (condition != null)
            {
                query = query.Where(condition);
            }
            var result = query.SingleAsync(cancellationToken);
            // result.Result will never be null, because EF Core will throw exception
            Logger.Trace(entityType + " entity (" + result.Result.ID + $") is read by GetSingleAsync with condition ({condition}).");
            return result;
        }
        /// <summary>
        /// Asynchronously, query a record from the database by using the long.
        /// <para>If there are more than one record, EF core will throw exception. </para>
        /// </summary>
        public Task<T> GetSingleAsync(
            long id,
            CancellationToken cancellationToken = default)
        {
            Logger.Trace($"GetSingleAsync Parameter: id ({id})");
            var result = DbSet.Where(x => x.ID == id).SingleAsync(cancellationToken);
            // result.Result will never be null, because EF Core will throw exception
            Logger.Trace(entityType + " entity (" + result.Result.ID.ToString(CultureInfo.InvariantCulture) + ") is read by GetSingleAsync(id).");
            return result;
        }
        /// <summary>
        /// Synchronously, query a record from the database with a condition LinQ expression.
        /// <para>If there is no record, it will return null. </para>
        /// <para>Example: GetSingleOrDefault(x => x.ID.ToString(CultureInfo.InvariantCulture) == "ABC");</para>
        /// </summary>
        public T GetSingleOrDefault(Expression<Func<T, bool>> condition = null)
        {
            Logger.Trace($"GetSingleOrDefault Parameter: condition ({condition})");
            IQueryable<T> query = DbSet;

            if (condition != null)
            {
                query = query.Where(condition);
            }
            var result = query.SingleOrDefault();
            if (result != null)
            {
                Logger.Trace(entityType + " entity (" + result.ID + $") is read by GetSingleOrDefault with condition ({condition}).");
            }
            else
            {
                Logger.Trace($"There is no record found in database by GetSingleOrDefault with condition ({condition}) ");
            }
            return result;
        }
        /// <summary>
        /// Synchronously, query a record from the database by using the long.
        /// <para>If there is no record, it will return null. </para>
        /// </summary>
        public T GetSingleOrDefault(long id)
        {
            Logger.Trace($"GetSingleOrDefault Parameter: id ({id})");
            var result = DbSet.Where(x => x.ID == id).SingleOrDefault();
            if (result != null)
            {
                Logger.Trace(entityType + " entity (" + result.ID.ToString(CultureInfo.InvariantCulture) + ") is read by GetSingleOrDefault(id).");
            }
            else
            {
                Logger.Trace("No record is found in the database via GetSingleOrDefault(id).");
            }
            return result;
        }
        /// <summary>
        /// Asynchronously, query a record from the database with a condition LinQ expression.
        /// <para>If there is no record, it will return null. </para>
        /// <para>Example: GetSingleOrDefaultAsync(x => x.ID.ToString(CultureInfo.InvariantCulture) == "ABC");</para>
        /// </summary>
        public Task<T> GetSingleOrDefaultAsync(
            Expression<Func<T, bool>> condition = null,
            CancellationToken cancellationToken = default)
        {
            Logger.Trace($"GetSingleOrDefaultAsync Parameter: condition ({condition})");
            IQueryable<T> query = DbSet;

            if (condition != null)
            {
                query = query.Where(condition);
            }

            var result = query.SingleOrDefaultAsync(cancellationToken);
            if (result.Result != null)
            {
                Logger.Trace(entityType + " entity (" + result.Result.ID.ToString(CultureInfo.InvariantCulture) + $") is read by GetSingleOrDefaultAsync with ({condition}).");
            }
            else
            {
                Logger.Trace($"There is no record found in database by GetSingleOrDefaultAsync with ({condition}) ");
            }
            return result;
        }
        /// <summary>
        /// Asynchronously, query a record from the database by using the long.
        /// <para>If there is no record, it will return null. </para>
        /// </summary>
        public Task<T> GetSingleOrDefaultAsync(
            long id,
            CancellationToken cancellationToken = default)
        {
            Logger.Trace($"GetSingleOrDefaultAsync Parameter: id ({id})");
            var result = DbSet.Where(x => x.ID == id).SingleOrDefaultAsync(cancellationToken);
            if (result.Result != null)
            {
                Logger.Trace(entityType + " entity (" + result.Result.ID + ") is read by GetSingleOrDefaultAsync(id)");
            }
            else
            {
                Logger.Trace("No record is found in the database via GetSingleOrDefaultAsync(id).");
            }
            return result;
        }
        /// <summary>
        /// Synchronously, query a list record from the database with a condition LinQ expression.
        /// <para><paramref name="condition"/> example: GetListAsync(x => x.DataState == DataState.Inactive);</para>
        /// <para><paramref name="orderBy"/> List of OrderBy objects that sort the returned list of records in ascending or descending order of a property denoted in OrderBy object. </para>
        /// <para><paramref name="pageIndex"/> Indicate the page index of the returned list of record. </para>
        /// <para><paramref name="pageSize"/> Indicate the size of each page for the returned list of record. </para>
        /// <para><paramref name="activeDatastate"/> Return a list of record that has DataState.Active. </para>
        /// </summary>
        public List<T> GetList(
            Expression<Func<T, bool>> condition = null,
            IList<OrderBy> orderBy = null,
            int pageIndex = 1,
            int pageSize = 10000,
            bool activeDatastate = true)
        {
            Logger.Trace($" GetList Parameter: condition ({condition}), orderBy ({orderBy}), pageIndex({pageIndex}), pageSize({pageSize}), activeDatastate({activeDatastate})");
            var query = DbSet;
            ExpressionStarter<T> predicate;
            List<T> resultList, resultListWithOrderBy;
            // checks if returned list of record needs to have DataState.active
            if (activeDatastate)
            {
                // Create predicate that selects record with Datastate.Active
                predicate = PredicateBuilder.New<T>(x => x.DataState == DataState.Active);

                if (condition != null)
                {
                    // AND condition with Datastate.Active predicate
                    predicate.And(condition);
                }
                if (orderBy != null && orderBy.Count > 0)
                {
                    // Gets a list of record filtered by predicate and OrderBy
                    resultListWithOrderBy = query.Where(predicate).OrderBy(orderBy).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
                    Logger.Trace("List of " + entityType + $" entities are read by GetList with OrderBy and activeDatastate({activeDatastate}), condition({condition}).");
                    return resultListWithOrderBy;
                }
                // Gets a list of record filtered by predicate
                resultList = query.Where(predicate).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
                Logger.Trace("List of " + entityType + $" entities are read by GetList with activeDatastate({activeDatastate}, condition({condition}).");
                return resultList;
            }
            // no DataState or Datastate.Active condition
            else
            {
                if (condition != null)
                {
                    // Create predicate for passed in condition parameter
                    predicate = PredicateBuilder.New<T>(condition);
                    if (orderBy != null && orderBy.Count > 0)
                    {
                        // Gets a list of record filtered by condition predicate and OrderBy
                        resultListWithOrderBy = query.Where(predicate).OrderBy(orderBy).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
                        Logger.Trace("List of " + entityType + $" entities are read by GetList with OrderBy and activeDatastate({activeDatastate}), condition({condition}.");
                        return resultListWithOrderBy;
                    }
                    // Gets a list of record filtered by predicate
                    resultList = query.Where(predicate).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
                    Logger.Trace("List of " + entityType + $" entities are read by GetList with activeDatastate({activeDatastate}), condition({condition}.");
                    return resultList;
                }
                else
                {
                    if (orderBy != null && orderBy.Count > 0)
                    {
                        //Gets a list of record with Orderby
                        resultListWithOrderBy = query.OrderBy(orderBy).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
                        Logger.Trace("List of " + entityType + $" entities are read by GetList with OrderBy and activeDatastate({activeDatastate}).");
                        return resultListWithOrderBy;
                    }
                    // Get a list of record
                    resultList = query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
                    Logger.Trace("List of " + entityType + $" entities are read by GetList with activeDatastate({activeDatastate}).");
                    return resultList;
                }
            }
        }
        /// <summary>
        /// Asynchronously, query a list record from the database with a condition LinQ expression.
        /// <para><paramref name="condition"/> example: GetListAsync(x => x.DataState == DataState.Inactive);</para>
        /// <para><paramref name="orderBy"/> List of OrderBy objects that sort the returned list of records in ascending or descending order of a property denoted in OrderBy object. </para>
        /// <para><paramref name="pageIndex"/> Indicate the page index of the returned list of record. </para>
        /// <para><paramref name="pageSize"/> Indicate the size of each page for the returned list of record. </para>
        /// <para><paramref name="activeDatastate"/> Return a list of record that has DataState.Active. </para>
        /// </summary>
        public async Task<List<T>> GetListAsync(
            Expression<Func<T, bool>> condition = null,
            IList<OrderBy> orderBy = null,
            int pageIndex = 1,
            int pageSize = 10000,
            bool activeDatastate = true,
            CancellationToken cancellationToken = default)
        {
            Logger.Trace($" GetListAsync Parameter: condition ({condition}), orderBy ({orderBy}), pageIndex({pageIndex}), pageSize({pageSize}), activeDatastate({activeDatastate})");
            var query = DbSet;
            ExpressionStarter<T> predicate;
            List<T> resultList, resultListWithOrderBy;
            // checks if returned list of record needs to have DataState.active
            if (activeDatastate)
            {
                // Create predicate that selects record with Datastate.Active
                predicate = PredicateBuilder.New<T>(x => x.DataState == DataState.Active);

                if (condition != null)
                {
                    // AND condition with Datastate.Active predicate
                    predicate.And(condition);
                }
                if (orderBy != null && orderBy.Count > 0)
                {
                    // Gets a list of record filtered by predicate and OrderBy
                    resultListWithOrderBy = await query.Where(predicate).OrderBy(orderBy).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(false);
                    Logger.Trace("List of " + entityType + $" entities are read by GetListAsync with OrderBy and activeDatastate({activeDatastate}), condition({condition}).");
                    return resultListWithOrderBy;
                }
                // Gets a list of record filtered by predicate
                resultList = await query.Where(predicate).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(false);
                Logger.Trace("List of " + entityType + $" entities are read by GetListAsync with activeDatastate({activeDatastate}, condition({condition}).");
                return resultList;
            }
            // no DataState or Datastate.Active condition
            else
            {
                if (condition != null)
                {
                    // Create predicate for passed in condition parameter
                    predicate = PredicateBuilder.New<T>(condition);
                    if (orderBy != null && orderBy.Count > 0)
                    {

                        // Gets a list of record filtered by condition predicate and OrderBy
                        resultListWithOrderBy = await query.Where(predicate).OrderBy(orderBy).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(false);
                        Logger.Trace("List of " + entityType + $" entities are read by GetListAsync with OrderBy and activeDatastate({activeDatastate}), condition({condition}.");
                        return resultListWithOrderBy;
                    }
                    // Gets a list of record filtered by predicate
                    resultList = await query.Where(predicate).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(false);
                    Logger.Trace("List of " + entityType + $" entities are read by GetListAsync with activeDatastate({activeDatastate}), condition({condition}.");
                    return resultList;
                }
                else
                {
                    if (orderBy != null && orderBy.Count > 0)
                    {
                        //Gets a list of record with Orderby
                        resultListWithOrderBy = await query.OrderBy(orderBy).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(false);
                        Logger.Trace("List of " + entityType + $" entities are read by GetListAsync with OrderBy and activeDatastate({activeDatastate}).");
                        return resultListWithOrderBy;
                    }
                    // Get a list of record
                    resultList = await query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(false);
                    Logger.Trace("List of " + entityType + $" entities are read by GetListAsync with activeDatastate({activeDatastate}).");
                    return resultList;
                }
            }
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                DbContext.Dispose();
            }
        }
    }
}
